<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="fhNxZ" id="fhNxZ"><span data-lake-id="u1065e216" id="u1065e216">典型回答</span></h1>
  <p data-lake-id="ua5850822" id="ua5850822"><br></p>
  <p data-lake-id="ude6e73af" id="ude6e73af"><br></p>
  <p data-lake-id="u5bfb7795" id="u5bfb7795"><span data-lake-id="u81a11e29" id="u81a11e29">因为索引底层是一个B+树，如果是联合索引的话，在构造B+树的时候，会先按照左边的key进行排序，左边的key相同时再依次按照右边的key排序。</span></p>
  <p data-lake-id="u0f640ba9" id="u0f640ba9"><br></p>
  <p data-lake-id="u7aa906a5" id="u7aa906a5"><span data-lake-id="ucce4a52f" id="ucce4a52f">所以，在通过索引查询的时候，也需要遵守最左前缀匹配的原则，也就是需要从联合索引的最左边开始进行匹配，这时候就要求查询语句的where条件中，包含最左边的索引的值。这也就是最左前缀匹配。</span></p>
  <p data-lake-id="u42a08883" id="u42a08883"><br></p>
  <p data-lake-id="u6686a24f" id="u6686a24f"><strong><span data-lake-id="u836516eb" id="u836516eb">MySQL一定是遵循最左前缀匹配的，这句话在以前是正确的，没有任何毛病。但是在MySQL 8.0中，就不一定了。因为8.0.13中引入了索引跳跃扫描。</span></strong></p>
  <p data-lake-id="u72bf02b6" id="u72bf02b6"><br></p>
  <h1 data-lake-id="xfrmF" id="xfrmF"><span data-lake-id="u20676482" id="u20676482">扩展知识</span></h1>
  <p data-lake-id="u7314b570" id="u7314b570"><br></p>
  <h2 data-lake-id="Mz3bk" id="Mz3bk"><span data-lake-id="ubae38a78" id="ubae38a78">索引跳跃扫描</span></h2>
  <p data-lake-id="u97063061" id="u97063061"><br></p>
  <p data-lake-id="u2982c79b" id="u2982c79b"><span data-lake-id="ud9de4d7f" id="ud9de4d7f">MySQL 8.0.13 版本中，对于range查询（什么是range后面会提到)，引入了索引跳跃扫描（Index Skip Scan）优化，支持不符合组合索引最左前缀原则条件下的SQL，依然能够使用组合索引，减少不必要的扫描。</span></p>
  <p data-lake-id="u54558927" id="u54558927"><br></p>
  <p data-lake-id="u7bcbf2f4" id="u7bcbf2f4"><span data-lake-id="u35bedf21" id="u35bedf21">通过一个例子给大家解释一下，首先有下面这样一张表（参考了MySQL官网的例子，但是我做了些改动和优化）：</span></p>
  <p data-lake-id="u09003123" id="u09003123"><br></p>
  <pre lang="java"><code>
CREATE TABLE t1 (f1 INT NOT NULL, f2 INT NOT NULL);
CREATE INDEX idx_t on t1(f1,f2);
INSERT INTO t1 VALUES
  (1,1), (1,2), (1,3), (1,4), (1,5),
  (2,1), (2,2), (2,3), (2,4), (2,5);
INSERT INTO t1 SELECT f1, f2 + 5 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 10 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 20 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 40 FROM t1;
</code></pre>
  <p data-lake-id="uec1b45eb" id="uec1b45eb"><br></p>
  <p data-lake-id="u001eea6d" id="u001eea6d"><span data-lake-id="uab100043" id="uab100043">通过上面的SQL，先创建一张t1表，并把f1,f2两个字段设置为联合索引。之后再向其中插入一些记录。</span></p>
  <p data-lake-id="uc13c7953" id="uc13c7953"><br></p>
  <p data-lake-id="u3f8f3708" id="u3f8f3708"><span data-lake-id="u2755fdeb" id="u2755fdeb">分别在MySQL 5.7.9和MySQL 8.0.30上执行</span><code data-lake-id="uad32e3ec" id="uad32e3ec"><span data-lake-id="u07a05fbd" id="u07a05fbd">EXPLAIN SELECT f1, f2 FROM t1 WHERE f2 = 40;</span></code></p>
  <p data-lake-id="u3a9a537b" id="u3a9a537b"><br></p>
  <p data-lake-id="u1a3a6691" id="u1a3a6691"><img src="https://www.hollischuang.com/wp-content/uploads/2022/12/16701385370311.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_35%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="uc4e7c7ed" id="uc4e7c7ed"><br></p>
  <p data-lake-id="u6c9f54f9" id="u6c9f54f9"><span data-lake-id="ua2f42bdc" id="ua2f42bdc">可以看到，主要有以下几个区别：</span></p>
  <p data-lake-id="ufce1c2e6" id="ufce1c2e6"><br></p>
  <blockquote data-lake-id="u78388531" id="u78388531">
   <p data-lake-id="uec7c5dda" id="uec7c5dda"><span data-lake-id="u96df4cce" id="u96df4cce">MySQL 5.7中，type = index，rows=160，extra=Using where;Using index</span></p>
   <p data-lake-id="u9029bd18" id="u9029bd18"><span data-lake-id="u726333ae" id="u726333ae"> </span></p>
   <p data-lake-id="ua34eb82d" id="ua34eb82d"><span data-lake-id="u6d2bd23d" id="u6d2bd23d">MySQL 8.0中，type = range，rows=16，extra=Using where;Using index for skip scan</span></p>
  </blockquote>
  <p data-lake-id="u5b499c17" id="u5b499c17"><br></p>
  <p data-lake-id="uf84184dc" id="uf84184dc"><span data-lake-id="uea3d992f" id="uea3d992f">这里面的type指的是扫描方式，range表示的是范围扫描，index表示的是索引树扫描，通常情况下，range要比index快得多。</span></p>
  <p data-lake-id="udb4d1c9e" id="udb4d1c9e"><br></p>
  <p data-lake-id="u8778e0cf" id="u8778e0cf"><span data-lake-id="u83bbaf71" id="u83bbaf71">从rows上也能看得出来，使用index的扫描方式共扫描了160行，而使用range的扫描方式只扫描了16行。</span></p>
  <p data-lake-id="u6641ca1c" id="u6641ca1c"><br></p>
  <p data-lake-id="uace2c474" id="uace2c474"><span data-lake-id="u60c18cf6" id="u60c18cf6">接着，重点来了，那就是为啥MySQL 8.0中的扫描方式可以更快呢？主要是因为</span><code data-lake-id="uee3cc1d5" id="uee3cc1d5"><span data-lake-id="u2a0a6c25" id="u2a0a6c25">Using index for skip scan</span></code><span data-lake-id="ub3794bcd" id="ub3794bcd"> 表示他用到了索引跳跃扫描的技术。</span></p>
  <p data-lake-id="u68eb0c36" id="u68eb0c36"><br></p>
  <p data-lake-id="udb8e6aab" id="udb8e6aab"><span data-lake-id="u0b5652f8" id="u0b5652f8">也就是说，虽然我们的SQL中，没有遵循最左前缀原则，只使用了f2作为查询条件，但是经过MySQL 8.0的优化以后，还是通过索引跳跃扫描的方式用到了索引了。</span></p>
  <p data-lake-id="u82b2fd90" id="u82b2fd90"><br></p>
  <h3 data-lake-id="b1T7v" id="b1T7v"><span data-lake-id="ufe1d473f" id="ufe1d473f">优化原理</span></h3>
  <p data-lake-id="u460a0b5f" id="u460a0b5f"><br></p>
  <p data-lake-id="u8dc4730f" id="u8dc4730f"><strong><span data-lake-id="u6b61ca85" id="u6b61ca85">那么他是怎么优化的呢？</span></strong></p>
  <p data-lake-id="u863745c7" id="u863745c7"><br></p>
  <p data-lake-id="u853134ba" id="u853134ba"><span data-lake-id="ufce388a7" id="ufce388a7">在MySQL 8.0.13 及以后的版本中，</span><code data-lake-id="u051492aa" id="u051492aa"><span data-lake-id="u21bc3016" id="u21bc3016">SELECT f1, f2 FROM t1 WHERE f2 = 40;</span></code><span data-lake-id="u7b174d6d" id="u7b174d6d">SQL执行过程如下：</span></p>
  <p data-lake-id="u2e8c0605" id="u2e8c0605"><br></p>
  <ul list="ucd84641a">
   <li fid="u84d8e7d5" data-lake-id="u3262c70e" id="u3262c70e"><span data-lake-id="u33e39433" id="u33e39433">获取f1字段第一个唯一值，也就是f1=1</span></li>
   <li fid="u84d8e7d5" data-lake-id="ub4bf0c02" id="ub4bf0c02"><span data-lake-id="u0cbc5901" id="u0cbc5901">构造f1=1 and f2 = 40，进行范围查询</span></li>
   <li fid="u84d8e7d5" data-lake-id="udec0ced6" id="udec0ced6"><span data-lake-id="u4ea57aa0" id="u4ea57aa0">获取f1字段第二个唯一值，也就是f1=2</span></li>
   <li fid="u84d8e7d5" data-lake-id="u4a60204d" id="u4a60204d"><span data-lake-id="uc1dd81a1" id="uc1dd81a1">构造f1=2 and f2 = 40，进行范围查询</span></li>
   <li fid="u84d8e7d5" data-lake-id="u5d2e8073" id="u5d2e8073"><span data-lake-id="u96920db1" id="u96920db1">一直扫描完f1字段所有的唯一值，最后将结果合并返回</span></li>
  </ul>
  <p data-lake-id="u96b4199e" id="u96b4199e"><br></p>
  <p data-lake-id="u50291411" id="u50291411"><span data-lake-id="u5df6c6ba" id="u5df6c6ba">也就是说，最终执行的SQL语句是</span><strong><span data-lake-id="ue6bf1d88" id="ue6bf1d88">像</span></strong><span data-lake-id="u72c28226" id="u72c28226">下面这样的：</span></p>
  <p data-lake-id="u2a80afa7" id="u2a80afa7"><br></p>
  <pre lang="java"><code>
SELECT f1, f2 FROM t1 WHERE f1 =1 and f2 = 40
UNION
SELECT f1, f2 FROM t1 WHERE f1 =2 and f2 = 40;
</code></pre>
  <p data-lake-id="u5b72c321" id="u5b72c321"><br></p>
  <p data-lake-id="u42ee6d28" id="u42ee6d28"><span data-lake-id="u050e6cc4" id="u050e6cc4">即，MySQL的优化器帮我们把联合索引中的f1字段作为查询条件进行查询了。</span></p>
  <p data-lake-id="u4ef2eef3" id="u4ef2eef3"><br></p>
  <h3 data-lake-id="1257a91b" id="1257a91b"><span data-lake-id="u63277636" id="u63277636">限制条件</span></h3>
  <p data-lake-id="ub27079b8" id="ub27079b8"><br></p>
  <p data-lake-id="u7aa17909" id="u7aa17909"><span data-lake-id="u6de7d555" id="u6de7d555">在知道了索引跳跃扫描的执行过程之后，很多聪明的读者其实就会发现，</span><strong><span data-lake-id="u8a0a6a90" id="u8a0a6a90">这种查询优化比较适合于f1的取值范围比较少，区分度不高的情况（如性别），一旦f1的区分度特别高的话（如出生年月日），这种查询可能会更慢。</span></strong></p>
  <p data-lake-id="ub33b985e" id="ub33b985e"><br></p>
  <p data-lake-id="u886a9a90" id="u886a9a90"><span data-lake-id="uc9a214b0" id="uc9a214b0">所以，</span><strong><span data-lake-id="uc5f88a9a" id="uc5f88a9a">真正要不要走索引跳跃扫描，还是要经过MySQL的优化器进行成本预估之后做决定的。</span></strong></p>
  <p data-lake-id="u50cb385e" id="u50cb385e"><br></p>
  <p data-lake-id="u56c6b65b" id="u56c6b65b"><span data-lake-id="u83866be7" id="u83866be7">所以，这种优化一般用于那种联合索引中第一个字段区分度不高的情况。但是话又说回来了，我们一般不太会把区分度不高的字段放在联合索引的左边，不过事无绝对，既然MySQL给了一个优化的方案，就说明还是有这样的诉求的。</span></p>
  <p data-lake-id="u49b60b2d" id="u49b60b2d"><br></p>
  <p data-lake-id="u6eba49b4" id="u6eba49b4"><strong><span data-lake-id="u06293d25" id="u06293d25">但是，我们不能依赖他这个优化，建立索引的时候，还是优先把区分度高的，查询频繁的字段放到联合索引的左边。</span></strong></p>
  <p data-lake-id="u79083228" id="u79083228"><br></p>
  <p data-lake-id="uf11388ae" id="uf11388ae"><span data-lake-id="u1536e49e" id="u1536e49e">除此之外，在MySQL官网中，还提到索引跳跃扫描还有一些其他的限制条件：</span></p>
  <p data-lake-id="ud1f27bb0" id="ud1f27bb0"><br></p>
  <ul list="u3e3d4da5">
   <li fid="u37f298cc" data-lake-id="ufd688450" id="ufd688450"><span data-lake-id="u32236a1a" id="u32236a1a">表T至少有一个联合索引，但是对于联合索引(A,B,C,D)来说，A和D可以是空的，但B和C必须是非空的。</span></li>
   <li fid="u37f298cc" data-lake-id="u76b1f9a5" id="u76b1f9a5"><span data-lake-id="u44ee0b61" id="u44ee0b61">查询必须只能依赖一张表，不能多表join</span></li>
   <li fid="u37f298cc" data-lake-id="u6fe1c6c8" id="u6fe1c6c8"><span data-lake-id="u06b0f939" id="u06b0f939">查询中不能使用GROUP BY或DISTINCT语句</span></li>
   <li fid="u37f298cc" data-lake-id="ub6be4229" id="ub6be4229"><span data-lake-id="u7a63a043" id="u7a63a043">查询的字段必须是索引中的列</span></li>
  </ul>
 </body>
</html>